ECS on Fargate構成でDocker Hubの認証情報を扱う
こんにちは!AWS事業本部コンサルティング部のたかくに(@takakuni_)です。
今回は、ECS on Fargate構成でDocker Hubの認証情報を扱うにはどうすればいいのかをご紹介します。
Docker Hubのレート制限
まず初めにDocker Hubには次のようなイメージ取得のレート制限があります。
- プル率の制限は個々のIPアドレスに基づく
- 匿名ユーザーの場合は、プル率の制限は1つのIPアドレスにつき、6時間ごとに100プル
- 認証ユーザーの場合は、プル率の制限は1つのIPアドレスにつき、6時間ごとに200プル
- 有償のDockerサブスクリプションのユーザーには制限がない
ダウンロード率制限 - Docker Hub におけるダウンロード率制限とは
どのIPが対象?
ECSの場合、IGW経由でDocker Hubと通信を行うパブリックIPがプル率の制限の対象になります。
ECSタスクをパブリックサブネットで運用している場合は「各タスクに割り当てたパブリックIP」、プライベートサブネットでNATゲートウェイを経由してDocker Hubと通信している場合は「NATゲートウェイに関連づけたElastic IP」になります。
同じような事象がより発生しやすいケースとしてCodeBuildが挙げられますが、VPC接続しないCodeBuildの場合はAWS IP アドレスの範囲で利用されるパブリックIPを経由するため発生しやすくなっています。
詳細は以下をご覧ください。
今回のケースだとCodeBuildよりはエラーが発生はしにくいですが、何かあった時に備えて設定方法を確認してみようと思います。
構成
構成の全体像は以下になります。Secrets Managerを使用して認証情報を管理し、タスク起動時にタスク実行ロールから参照する流れになります。
今回は検証のため、ECSタスクはパブリックサブネット上に配置し、直接Docker Hubと通信を行うようします。
今回利用したコードは以下のレポジトリに格納されています。自由にカスタマイズいただけると幸いです。
Docker Hubの認証情報を取得
はじめにSecrets Managerに保管するDocker Hubの認証情報を取得します。
私の場合、Docker HubでMFAを有効化しているため、パーソナルアクセストークンを払い出しました。
アクセストークンの管理より引用
Secrets Managerシークレットの保管
今回設定する方法の場合、Systems Managerは使えず、Secrets Managerが必須になります。
アクセストークンを次の形式で保管します。
{
"username" : "privateRegistryUsername",
"password" : "privateRegistryPassword"
}
つまづきポイント
シークレットはダブルクォーテーションで囲む必要があります。
まずは、失敗例です。以下のようにシングルクォーテーションでシークレットを定義したとします。
{
"username" : "privateRegistryUsername",
"password" : "privateRegistryPassword"
}
この場合、Secrets Manager側でキー/バリューの認識がうまくいかないため、username, passwordがうまく渡せない状態になります。
そのため、ECSタスクはプロビジョニングを試みるものの、以下のエラーで停止されてしまいます。同じようなエラーが出た場合は、シークレット側も見てみるといいかもしれないです。
Resourceinitializationerror: unable to pull secrets or registry auth: execution resource retrieval failed: unable to get registry auth from asm: service call has been retried 1 time(s): unable to unmarshal secret value of authorization data from asm: invalid character '\'' looking for beginning of object key string
Terraformでダブルクォーテーションエラーを避けつつ、定義する方法が2通り思いついたためご紹介します。
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version
resource "aws_secretsmanager_secret_version" "dockerhub" {
secret_id = aws_secretsmanager_secret.dockerhub.id
secret_string = jsonencode({
username = var.dockerhub_username
password = var.dockerhub_password
})
}
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version
resource "aws_secretsmanager_secret_version" "dockerhub" {
secret_id = aws_secretsmanager_secret.dockerhub.id
secret_string = "{\"username\":\"${var.dockerhub_username}\",\"password\":\"${var.dockerhub_password}\"}"
}
IAMポリシーの作成
タスク実行ロールには、「AmazonECSTaskExecutionRolePolicy」ポリシーを付与することが多いかと思いますが、以下の権限が不足しているため、カスタマー管理ポリシーを新たに作成して追加します。
- Secrets Managerで保管したシークレットの値を取得する権限
- Secrets Managerで保管したシークレットを暗号化している鍵に対して復号できる権限(カスタマー管理キーを利用する場合は必要)
具体的に表すと次のような管理ポリシーが想定されます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"ssm:GetParameters"
],
"Resource": [
"arn:aws:secretsmanager:<region>:<aws_account_id>:secret:secret_name",
"arn:aws:kms:<region>:<aws_account_id>:key/key_id"
]
}
]
}
タスク定義の作成
レジストリの認証は、タスク定義内で定義する必要があります。タスク定義内に「repositoryCredentials」プロパティを追加して、認証情報として利用するシークレットARNを指定します。
"containerDefinitions": [
{
"image": "private-repo/private-image",
"repositoryCredentials": {
"credentialsParameter": "arn:aws:secretsmanager:region:aws_account_id:secret:secret_name"
}
}
]
動作確認
認証情報が使われているのかを検証しするために、ダミーと正規パスワードの2パターンを検証します。
ダミーパスワード
シークレットの内容を以下のように、ダミーユーザー名/パスワードに設定します。
{
"username" : "privateRegistryUsername",
"password" : "privateRegistryPassword"
}
タスクを起動すると想定通り、ダミーパスワードのため以下のエラーが返されました。
Cannotpullcontainererror: pull image manifest has been retried 1 time(s): failed to resolve ref docker.io/vulnerables/web-dvwa:latest: failed to authorize: failed to fetch oauth token: unexpected status: 401 Unauthorized
正規パスワードに変更
先ほどのシークレットの部分を正規のユーザー名/パスワードに変更します。
問題なくタスクが起動できていることがわかりました。
参考
まとめ
以上、「ECS on Fargate構成でDocker Hubの認証情報を扱う」でした。
いざという時に備えてのブログでしたが、シークレットの保管方法など学びになったなと思う部分がたくさんありました。
この記事がどなたかの参考になれば幸いです。
AWS事業本部コンサルティング部のたかくに(@takakuni_)でした!